﻿// <copyright file="RainbowBox.cs" company="Oleg V. Polikarpotchkin">
// Copyright © 2009 Oleg V. Polikarpotchkin. All Right Reserved
// </copyright>
// <author>Oleg V. Polikarpotchkin</author>
// <email>ov-p@yandex.ru</email>
// <date>2009-06-07</date>
// <summary>OpenWPFChart library.
// ColorPickerControls. RainbowBox element. Clone of the Windows Forms ColorDialog spectrum color picker component.
// </summary>
// <revision>$Id$</revision>

using System.Diagnostics;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;

namespace OpenWPFChart.Helpers.ChooseColorDialog
{
	/// <summary>
	/// RainbowBox element.
	/// </summary>
	/// <remarks>
	/// Clone of the Windows Forms ColorDialog spectrum color picker component.
	/// </remarks>
	public class RainbowBox : FrameworkElement
	{
		#region SelectedColor
		/// <summary>
		/// SelectedColor property backing ReadOnly DependencyProperty.
		/// </summary>
		private static readonly DependencyPropertyKey SelectedColorPropertyKey
			= DependencyProperty.RegisterReadOnly("SelectedColor", typeof(Color), typeof(RainbowBox)
			, new PropertyMetadata(Colors.Transparent));
		/// <summary>
		/// Gets the color selected.
		/// </summary>
		/// <value>The color selected.</value>
		public Color SelectedColor
		{
			get { return (Color)GetValue(SelectedColorPropertyKey.DependencyProperty); }
			private set { SetValue(SelectedColorPropertyKey, value); }
		}

		/// <summary>
		/// Updates the color selected according to the current selector position.
		/// </summary>
		void UpdateSelectedColor()
		{
			GradientStopCollection rainbow = Rainbow;
			Color clr = Colors.Transparent;
			if (rainbow.Count == 1)
			{
				clr = rainbow[0].Color;
			}
			else
			{
				double x = SelectorPosition.X;
				int i = 1;
				for (; i < rainbow.Count; i++)
				{
					if (rainbow[i].Offset >= x)
					{
						float delta = (float)((x - rainbow[i - 1].Offset) 
							/ (rainbow[i].Offset - rainbow[i - 1].Offset));
						clr = rainbow[i - 1].Color * (1 - delta) + rainbow[i].Color * delta;
						break;
					}
				}
				if (i == rainbow.Count)
					clr = rainbow[rainbow.Count - 1].Color;
			}

			float y = (float)SelectorPosition.Y;
			clr = clr * (1f - y) + Colors.Gray * y;

			SelectedColor = clr;
			RaiseEvent(new SelectedColorChangedEventArgs(clr, SelectedColorChangedEvent, this));
		}

		/// <summary>
		/// Updates the selector position according to the <paramref name="color"/> specified.
		/// </summary>
		/// <param name="color">The color.</param>
		/// <returns><c>true</c> if provided <paramref name="color"/> value is set; otherwise <c>false</c>.</returns>
		/// <remarks>
		/// The method finds the RainbowBox position basing on the given <paramref name="color"/>
		/// Hue and Saturation values. If such a position is found the selector is set to it and
		/// the method returns <c>true</c>; otherwise the method returns <c>false</c>.
		/// </remarks>
		public bool TrySetSelectedColor(Color color)
		{
			HSL hsl = new HSL(color);

			// Get the color spectrum.
			GradientStopCollection rainbow = Rainbow;
			Debug.Assert(rainbow.Count > 0, "rainbow.Count > 0");

			// Find the x position by the Hue.
			double x = 0;
			if (rainbow.Count == 1)
			{
				HSL rainbowHslColor = new HSL(rainbow[0].Color);
				if (rainbowHslColor.Hue != hsl.Hue)
					return false; // Matched color not found!
			}
			else
			{
				HSL hslPrev = new HSL(rainbow[0].Color);
				int i = 1;
				for (; i < rainbow.Count; i++)
				{
					HSL hslCurr = new HSL(rainbow[i].Color);
					if (hsl.Hue >= hslPrev.Hue && hsl.Hue <= hslCurr.Hue)
					{ // Interpolate the Hue.
						double delta = (hsl.Hue - hslPrev.Hue) / (hslCurr.Hue - hslPrev.Hue);
						x = rainbow[i - 1].Offset * (1 - delta) + rainbow[i].Offset * delta;
						break;
					}
					hslPrev = hslCurr;
				}
				if (i == rainbow.Count)
					return false; // Matched color not found!
			}
			Debug.Assert(x >= 0.0, "x >= 0.0");
			Debug.Assert(x <= 1.0, "x <= 1.0");
			// y position is the (1 - Saturation).
			//Debug.Assert(hsl.Saturation >= 0.0, "hsl.Saturation >= 0.0");
			//Debug.Assert(hsl.Saturation <= 1.0, "hsl.Saturation <= 1.0");

			// Set the RainbowBox selector position and, so, the SelectedColor.
			SelectorPosition = new Point(x, 1.0 - hsl.Saturation);
			return true;
		}
		#endregion SelectedColor

		#region SelectorPosition
		/// <summary>
		/// SelectorPosition backing DependencyProperty.
		/// </summary>
		private static readonly DependencyProperty SelectorPositionProperty
			= DependencyProperty.Register("Position", typeof(Point), typeof(RainbowBox)
				, new FrameworkPropertyMetadata(new Point()
					, FrameworkPropertyMetadataOptions.AffectsRender
					, SelectorPositionPropertyChanged)
				, ValidateSelectorPosition);
		/// <summary>
		/// Gets or sets the Selector Position.
		/// </summary>
		/// <value>The Point instance restricted to the [0,1 0,1] rectangle.</value>
		/// <remarks>The selector position falls into the [0,1 0,1] rectangle.</remarks>
		public Point SelectorPosition
		{
			get { return (Point)GetValue(SelectorPositionProperty); }
			set { SetValue(SelectorPositionProperty, value); }
		}
		/// <summary>
		/// Validates the selector position.
		/// </summary>
		/// <param name="value">The value.</param>
		/// <returns></returns>
		/// <remarks>The selector position must fall into the [0,1 0,1] rectangle.</remarks>
		static bool ValidateSelectorPosition(object value)
		{
			if (value == null || value.GetType() != typeof(Point))
				return false;
			Point pt = (Point)value;
			if (pt.X < 0.0 || pt.X > 1.0 || pt.Y < 0.0 || pt.Y > 1.0)
				return false;
			return true;
		}
		/// <summary>
		/// SelectorPositions Property Changed event handler.
		/// </summary>
		/// <param name="d">The sender.</param>
		/// <param name="e">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> 
		/// instance containing the event data.</param>
		static void SelectorPositionPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			RainbowBox ths = d as RainbowBox;
			if (ths == null)
				return;
			ths.UpdateSelectedColor();
		}

		/// <summary>
		/// Get the Selector Position restricted by the element bounds and 
		/// normalized to [0,1 0,1] rectangle.
		/// </summary>
		/// <param name="point">The point in the element windows coordinates.</param>
		/// <returns>The point in [0,1 0,1] rectangle.</returns>
		void UpdateSelectorPosition(Point point)
		{
			double x = point.X, y = point.Y;

			if (x < 0)
				x = 0;
			else if (x > ActualWidth)
				x = ActualWidth;
			x /= ActualWidth;

			if (y < 0)
				y = 0;
			else if (y > ActualHeight)
				y = ActualHeight;
			y /= ActualHeight;

			SelectorPosition = new Point(x, y);
		}

		/// <summary>
		/// Sets the <paramref name="pt"/> as the new position if the point falls 
		/// into the element bounds.
		/// </summary>
		/// <param name="pt">The point in the element window coordinates.</param>
		void UpdateSelectorPositionIfInBounds(Point pt)
		{
			if (pt.X >= 0 && pt.X <= ActualWidth && pt.Y >= 0 && pt.Y <= ActualHeight)
				UpdateSelectorPosition(pt);
		}
		#endregion Position

		#region Rainbow
		// Default Rainbow gradient.
		static GradientStopCollection defaultRainbow = new GradientStopCollection()
		{
			new GradientStop(Colors.Red, 0),
			new GradientStop(Colors.Orange, 1.0 / 7.0),
			new GradientStop(Colors.Yellow, 2.0 / 7.0),
			new GradientStop(Colors.Lime, 3.0 / 7.0),
			new GradientStop(Colors.Blue, 4.0 / 7.0),
			new GradientStop(Colors.Indigo, 5.0 / 7.0),
			new GradientStop(Colors.Violet, 6.0 / 7.0),
			new GradientStop(Colors.Red, 1),
		};
		/// <summary>
		/// Backing Rainbow DependencyProperty.
		/// </summary>
		public static readonly DependencyProperty RainbowProperty
			= DependencyProperty.Register("Rainbow", typeof(GradientStopCollection), typeof(RainbowBox),
				new FrameworkPropertyMetadata(defaultRainbow
					, FrameworkPropertyMetadataOptions.AffectsRender
					, RainbowPropertyChanged)
				, RainbowValidate);
		/// <summary>
		/// Gets oe sets the RainbowBrush.
		/// </summary>
		/// <value>The LinearGradientBrush.</value>
		public GradientStopCollection Rainbow
		{
			get { return (GradientStopCollection)GetValue(RainbowProperty); }
			private set { SetValue(RainbowProperty, value); }
		}
		/// <summary>
		/// Validate the proposed value.
		/// </summary>
		/// <param name="value">The value.</param>
		/// <returns></returns>
		/// <remarks>
		/// Value of Rainbow property must not be empty.
		/// </remarks>
		static bool RainbowValidate(object value)
		{
			if (value == null || (value as GradientStopCollection).Count == 0)
				return false;
			return true;
		}
		/// <summary>
		/// RainbowProperty changed event handler.
		/// </summary>
		/// <param name="d">The d.</param>
		/// <param name="e">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> 
		/// instance containing the event data.</param>
		static void RainbowPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			RainbowBox ths = d as RainbowBox;
			if (ths == null)
				return;

			if (!ths.TrySetSelectedColor(ths.SelectedColor))
				ths.SelectorPosition = new Point();
		}
		#endregion Rainbow

		#region Selector
		/// <summary>
		/// Selector property backing DependencyProperty.
		/// </summary>
		public static readonly DependencyProperty SelectorProperty
			= DependencyProperty.Register("Selector", typeof(Drawing), typeof(RainbowBox)
			, new FrameworkPropertyMetadata(new GeometryDrawing(Brushes.White, new Pen(Brushes.Black, 1)
					, new EllipseGeometry(new Point(), 5, 5))
				, FrameworkPropertyMetadataOptions.AffectsRender)
				, ValidateSelector);
		/// <summary>
		/// Validates the suggested selector drawing value.
		/// </summary>
		/// <param name="value">The value.</param>
		/// <returns><c>true</c> if suggested value isn't null; otherwise <c>false</c>.</returns>
		static bool ValidateSelector(object value)
		{
			return value == null ? false : true;
		}
		/// <summary>
		/// Gets or sets the selector drawing.
		/// </summary>
		/// <value>The selector drawing. Must not be null</value>
		/// <remarks>
		/// <see cref="SelectorDrawing"/> is the <see cref="System.Windows.Media.Drawing"/> using
		/// to mark the position where the color is selected.
		/// <para>The <see cref="ImageColorPicker"/> desn't pose any restrictions on the size of
		/// this drawing. It's the user duty to choose it resonably.</para>
		/// </remarks>
		public Drawing Selector
		{
			get { return (Drawing)GetValue(SelectorProperty); }
			set { SetValue(SelectorProperty, value); }
		}
		#endregion Selector

		#region SelectedColorChangedEvent
		/// <summary>
		/// SelectedColorChangedEvent backing RoutedEvent.
		/// </summary>
		public static readonly RoutedEvent SelectedColorChangedEvent
			= EventManager.RegisterRoutedEvent("SelectedColorChanged", RoutingStrategy.Bubble
				, typeof(SelectedColorChangedEventHandler), typeof(RainbowBox));
		/// <summary>
		/// SelectedColorChangedEvent accessors.
		/// </summary>
		public event SelectedColorChangedEventHandler SelectedColorChanged
		{
			add { AddHandler(SelectedColorChangedEvent, value); }
			remove { RemoveHandler(SelectedColorChangedEvent, value); }
		}
		#endregion SelectedColorChangedEvent

		/// <summary>
		/// Renders the background and the SelectorDrawing.
		/// </summary>
		/// <param name="dc">An instance of <see cref="T:System.Windows.Media.DrawingContext"/> 
		/// used to render the control.</param>
		protected override void OnRender(DrawingContext dc)
		{
			base.OnRender(dc);

			if (ActualWidth == 0 || ActualHeight == 0)
				return;

			if (SelectedColor == Colors.Transparent)
				UpdateSelectedColor();

			// Draw background.
			// Horizontal Rainbow.
			dc.DrawRectangle(new LinearGradientBrush()
					{
						StartPoint = new Point(0, 0.5),
						EndPoint = new Point(1, 0.5),
						GradientStops = Rainbow
					}
				, null, new Rect(0, 0, ActualWidth, ActualHeight));
			// Vertical Gray gradient.
			dc.DrawRectangle(new LinearGradientBrush()
					{
						StartPoint = new Point(0.5, 0),
						EndPoint = new Point(0.5, 1),
						GradientStops = new GradientStopCollection()
						{
							new GradientStop(Colors.Transparent, 0),
							new GradientStop(Colors.Gray, 1)
						}
					}
				, null, new Rect(0, 0, ActualWidth, ActualHeight));

			// Render the SelectorDrawing
			dc.PushTransform(new TranslateTransform(SelectorPosition.X * ActualWidth
				, SelectorPosition.Y * ActualHeight));
			dc.DrawDrawing(Selector);
			dc.Pop();
		}

		#region Mouse gesture handling
		protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
		{
			base.OnMouseLeftButtonDown(e);
			UpdateSelectorPositionIfInBounds(e.GetPosition(this));
		}

		protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
		{
			base.OnMouseLeftButtonUp(e);
			UpdateSelectorPositionIfInBounds(e.GetPosition(this));
		}

		protected override void OnMouseMove(MouseEventArgs e)
		{
			base.OnMouseMove(e);
			if (e.LeftButton == MouseButtonState.Pressed)
				UpdateSelectorPositionIfInBounds(e.GetPosition(this));
		}

		protected override void OnMouseEnter(MouseEventArgs e)
		{
			base.OnMouseEnter(e);
			if (e.LeftButton == MouseButtonState.Pressed)
				UpdateSelectorPosition(e.GetPosition(this));
		}
		#endregion Mouse gesture handling
	}
}
